﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.DirectoryServices;

using Microsoft.Win32;

using IWshRuntimeLibrary;

using SysFile = System.IO.File;

using King.Extensions;
using King.TeamFoundation;
using King.Reflection;
using King.Text;

namespace King.Automation.Developer {

    public class Program {
        public static int Main(string[] args) {
            return ConsoleApplication.EntryPoint(args);
        }
    }

    public enum Font {
        Terminal,
        Consolas,
        LucidaConsole,
    }

    // shortcut /name=tfs /arguments=afylab.bat /startDirectory=c:\tfs\env\
    /// <summary>
    /// Creates a shortcut of the command shell with a custom configuration or
    /// set the defaults for command shell shortcuts.
    /// </summary>
    [Command]
    public class Shortcut : Command {

        private class Configuration {

            private class KeyAttribute : Attribute {
                public string Name { get; set; }
                public bool FirstShort { get; set; }
                public bool SecondShort { get; set; }
            }

            internal Configuration(Shortcut shortcut) {
                var pairs =
                    from o in GetType().Get<PropertyInfo>()
                    join x in typeof(Shortcut).Get<PropertyInfo>() on o.Name equals x.Name
                    select new { o, x };

                foreach (var pair in pairs)
                    pair.o.SetValue(this, pair.x.GetValue(shortcut));
            }
            internal Configuration() {
                var console = Registry.CurrentUser.OpenSubKey("Console");

                foreach (var property in Properties()) {
                    var attribute = property.GetAttribute<KeyAttribute>();
                    var type = property.PropertyType.UnNullify();
                    var name = attribute.Name ?? property.Name;
                    var value = console.GetValue(name);

                    if (value == null || string.Empty.Equals(value))
                        continue;

                    if (type == typeof(bool))
                        value = (int)value == 1;
                    else if (type.IsEnum)
                        value = Enum.Parse(type, ((string)value).Remove(" "));
                    else if (type == typeof(int)) {
                        if (attribute.FirstShort)
                            value = (int)value & 0xFFFF;
                        else if (attribute.SecondShort)
                            value = (int)value >> 4 * 4;
                    }

                    property.SetValue(this, value);
                }
            }

            private IEnumerable<PropertyInfo> Properties() {
                var result =
                    from o in GetType().Get<PropertyInfo>()
                    where o.IsDefined<KeyAttribute>()
                    select o;
                return result;
            }

            public void Save() {
                var console = Registry.CurrentUser.OpenSubKey("Console", true);

                if (Font == King.Automation.Developer.Font.Terminal)
                    throw new ArgumentException("Terminal not an acceptable font value.");

                else if (Font != null)
                    FontFamily = 54;

                foreach (var property in Properties()) {
                    var attribute = property.GetAttribute<KeyAttribute>();
                    var type = property.PropertyType.UnNullify();
                    var name = attribute.Name ?? property.Name;
                    var value = property.GetValue(this);

                    if (value == null)
                        continue;

                    var kind = RegistryValueKind.DWord;

                    if (type == typeof(bool)) {
                        value = (bool)value ? 1 : 0;
                        kind = RegistryValueKind.DWord;

                    } else if (type == typeof(string)) {
                        kind = RegistryValueKind.String;

                    } else if (type.IsEnum) {
                        value = value.ToString();
                        kind = RegistryValueKind.String;

                    } else if (type == typeof(int)) {
                        var oldValue = (int)console.GetValue(name);
                        kind = RegistryValueKind.DWord;

                        if (attribute.FirstShort)
                            value = (int)value | (oldValue & 0xFFFF0000);
                        else if (attribute.SecondShort)
                            value = (int)value << 4 * 4 | (oldValue & 0x0000FFFF);
                    }

                    console.SetValue(name, value, kind);
                }
            }

            [Key]
            private int? FontFamily { get; set; }
            [Key(Name = "FontSize", FirstShort = true)]
            private int FontSizeZero { get; set; }

            [Key(Name = "FaceName")]
            public Font? Font { get; set; }

            [Key(SecondShort = true)]
            public int? FontSize { get; set; }

            [Key(Name = "WindowSize", FirstShort = true)]
            public int? WindowWidth { get; set; }
            [Key(Name = "WindowSize", SecondShort = true)]
            public int? WindowHeight { get; set; }

            [Key(Name = "ScreenBufferSize", FirstShort = true)]
            public int? BufferWidth { get; set; }
            [Key(Name = "ScreenBufferSize", SecondShort = true)]
            public int? BufferHeight { get; set; }

            [Key]
            public bool? QuickEdit { get; set; }

            [Key]
            public int? HistoryBufferSize { get; set; }

            [Key(Name = "ScreenColors")]
            public int? Color { get; set; }
        }

        protected override IEnumerable<object> Execute(TextNodeContext context) {
            var originalConfiguration = new Configuration();

            try {

                var configuration = new Configuration(this);
                configuration.Save();

                if (Name != null) {
                    var path = Name + ".lnk";
                    if (!Path.IsPathRooted(Name))
                        path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop).NormalizeDirectory() + path;

                    if (SysFile.Exists(path))
                        SysFile.Delete(path);

                    var shell = new WshShell();
                    var shortcut = (IWshShortcut)shell.CreateShortcut(path);
                    shortcut.TargetPath = TargetPath;
                    shortcut.WorkingDirectory = StartDirectory;
                    shortcut.Arguments = Arguments;
                    shortcut.Save();
                }

            } finally {
                if (SetDefault != true)
                    originalConfiguration.Save();
            }

            return null;
        }

        /// <summary>
        /// Set command shell environment configuration as the default. If set
        /// Name, TargetPath, Arguments, and StartDirectory can be omitted.
        /// </summary>
        [CommandSwitch(1)]
        public bool? SetDefault { get; set; }

        /// <summary>
        /// The shortcut of the target. 
        /// </summary>
        [CommandSwitch(2, DefaultValue = @"%SystemRoot%\system32\cmd.exe")]
        public string TargetPath { get; set; }

        /// <summary>
        /// The full path to the shortcut without extension. If the name is not
        /// fully qualified then the shortcut will be placed on the desktop.
        /// </summary>
        [CommandSwitch(3)]
        public string Name { get; set; }

        [CommandSwitch(4)]
        public string Arguments { get; set; }

        [CommandSwitch(5, IsDirectory = true)]
        public string StartDirectory { get; set; }

        /// <summary>
        /// The command shell font.
        /// </summary>
        [CommandSwitch(6, DefaultValue = King.Automation.Developer.Font.Consolas)]
        public Font Font { get; set; }

        /// <summary>
        /// The font size. Be sure to pass a valid value for the active font if
        /// you specify a font.
        /// </summary>
        [CommandSwitch(7, DefaultValue = 14)]
        public int FontSize { get; set; }

        [CommandSwitch(8, DefaultValue = 120)]
        public int WindowWidth { get; set; }

        [CommandSwitch(9, DefaultValue = 40)]
        public int WindowHeight { get; set; }

        [CommandSwitch(10, DefaultValue = 400)]
        public int BufferWidth { get; set; }

        [CommandSwitch(11, DefaultValue = 9999)]
        public int BufferHeight { get; set; }

        [CommandSwitch(12, DefaultValue = true)]
        public bool QuickEdit { get; set; }

        [CommandSwitch(13, DefaultValue = 999)]
        public int HistoryBufferSize { get; set; }

        [CommandSwitch(14, DefaultValue = 0x07)]
        public int Color { get; set; }
    }

    /// <summary>
    /// Emit a bat file to set an environment variable and\or set machine\user environment variables. 
    /// </summary>
    [Command]
    public class Set {

        public void Execute() {

            var value = Environment.GetEnvironmentVariable(Name, EnvironmentVariableTarget.Process) ??
                Environment.GetEnvironmentVariable(Name, EnvironmentVariableTarget.User) ??
                Environment.GetEnvironmentVariable(Name, EnvironmentVariableTarget.Machine);

            if (string.IsNullOrEmpty(value) || !UseExisting) {

                if (Prompt != null) {

                    while (true) {
                        Console.Write(Prompt);
                        value = Console.ReadLine();

                        if (Choice.Any() && !Choice.Contains(value)) {
                            Console.WriteLine("Please enter a valid value: " + Choice.StringJoin(", ", o => o));
                            continue;
                        }

                        break;
                    }
                }

                if (string.IsNullOrEmpty(value))
                    value = Value;
            }

            if (IsDirectory)
                value = value.NormalizeDirectory();

            if (Machine)
                Environment.SetEnvironmentVariable(Name, value, EnvironmentVariableTarget.Machine);

            if (User)
                Environment.SetEnvironmentVariable(Name, value, EnvironmentVariableTarget.User);

            var set = "SET {0}={1}".Substitute(Name, value);
            if (File != null)
                using (var sw = new StreamWriter(File, false))
                    sw.WriteLine(set);

            Console.WriteLine(set);
        }

        /// <summary>
        /// Name of the environment variable to set.
        /// </summary>
        [CommandTarget(1)]
        public string Name { get; set; }

        /// <summary>
        /// Value of environment variable. 
        /// </summary>
        [CommandSwitch(1)]
        public string Value { get; set; }

        /// <summary>
        /// Set a Machine wide environment variable.
        /// </summary>
        [CommandSwitch(2)]
        public bool Machine { get; set; }

        /// <summary>
        /// Set an environment variable for the current user.
        /// </summary>
        [CommandSwitch(3)]
        public bool User { get; set; }

        /// <summary>
        /// A prompt to display asking the user to enter an environment variable.
        /// If no value is provided the value will be taken from the /value switch.
        /// </summary>
        [CommandSwitch(4)]
        public string Prompt { get; set; }

        /// <summary>
        /// A file to emit which if executed will set the environment variable. For example:
        ///     afysetup set FOO /value:BAR /file:%TEMP%temp.bat
        ///     CALL %TEMP%temp.bat & ERASE %TEMP%temp.bat
        /// </summary>
        [CommandSwitch(5)]
        public string File { get; set; }

        /// <summary>
        /// If set the value will be normalized to have a trailing backslash.
        /// </summary>
        [CommandSwitch(6)]
        public bool IsDirectory { get; set; }

        /// <summary>
        /// Only set the value if it's not already defined. If /isDirectory is set and 
        /// an existing value exists then it may have a trailing slash appeneded to it.
        /// </summary>
        [CommandSwitch(6)]
        public bool UseExisting { get; set; }

        [CommandSwitch(7)]
        public string[] Choice { get; set; }
    }

    /// <summary>
    /// Set or clear TFS setting to diff files when they are doubled clicked in dialogues.
    /// </summary>
    [Command]
    public class ClickToDiff {

        private void Execute() {

            var user = Registry.CurrentUser;
            var visualStudio = user.OpenSubKeyPath(@"Software\Microsoft\VisualStudio\10.0\TeamFoundation\SourceControl\Behavior", true);
            if (visualStudio == null) {
                Console.WriteLine(@"ERROR: Regkey not found: HKCU\Software\Microsoft\VisualStudio\10.0\TeamFoundation\SourceControl\Behavior");
                return;
            }

            visualStudio.SetValue("DoubleClickOnChange", Disable ? 0 : 1, RegistryValueKind.DWord);
        }

        /// <summary>
        /// If set disable the setting, else enable.
        /// </summary>
        [CommandSwitch(1)]
        public bool Disable { get; set; }
    }

    internal class AlertTypeAttribute : Attribute {
        private string m_eventType;

        internal AlertTypeAttribute(string eventType) {
            m_eventType = eventType;
        }

        public string EventType {
            get { return m_eventType; }
        }
    }
    public enum AlertType {
        /// <summary>Anything is checked in.</summary>
        [AlertType("CheckinEvent")]
        CheckIn,

        /// <summary>My work items are changed by others.</summary>
        [AlertType("WorkItemChangedEvent")]
        MyWorkItemChanged,

        /// <summary>A build quality changes.</summary>
        [AlertType("BuildStatusChangeEvent")]
        BuildQualityChanges,

        /// <summary>Any build completes.</summary>
        [AlertType("BuildCompletionEvent2")]
        AnyBuildCompletes,

        /// <summary>My build completes.</summary>
        [AlertType("BuildCompletionEvent2")]
        MyBuildCompletes
    }

    /// <summary>
    /// Enable TFS alerts for changing work items, checkins, and build completes and quality changes.
    /// </summary>
    [Command]
    public class Alert {

        private static string SortChars(string source) {
            var chars = source.ToCharArray().Distinct().OrderBy(o => o);
            var result = chars.Aggregate(string.Empty, (a, c) => a + c);
            return result;
        }

        private void Execute() {

            foreach (var alert in Alerts) {
                var id = EnableAlert(alert);
                if (Disable)
                    DisableAlert(id);
            }
        }

        private void DisableAlert(int id) {
            throw new NotImplementedException();
        }
        private int EnableAlert(AlertType alert) {

            var attribute = alert.GetField().GetAttribute<AlertTypeAttribute>();

            var filter = string.Empty;

            if (alert == AlertType.CheckIn)
                filter = CheckinFilter();

            else if (alert == AlertType.MyWorkItemChanged)
                filter = MyWorkItemChangedFilter();

            else if (alert == AlertType.AnyBuildCompletes)
                filter = TeamProjectFilter();

            else if (alert == AlertType.BuildQualityChanges)
                filter = TeamProjectFilter();

            else if (alert == AlertType.MyBuildCompletes)
                filter = MyBuildCompletesFilter();

            else throw new ArgumentException();

            var bisSubscribe =
                new BisSubscribe() {
                    Address = Email,
                    Collection = Collection,
                    DeliveryType = DeliveryType.EmailHtml,
                    EventType = attribute.EventType,
                    Filter = filter,
                }.ReadToEnd();

            var id = Regex.Match(bisSubscribe, @"The subscription ID is (?<id>\d*)").Get<int>("id");
            return id;
        }

        private string Translate(string text) {
            var orderdCharsLower = SortChars(Project).ToLower();
            var orderdCharsUpper = SortChars(Project).ToUpper();

            return "translate({0}, '{1}', '{2}') = '{3}'"
                .Substitute(text, orderdCharsLower, orderdCharsUpper, Project.ToUpper());
        }
        private string MyBuildCompletesFilter() {
            var teamProjectFilter = TeamProjectFilter();
            var filter = teamProjectFilter + "  AND \"RequestedFor\" = '{0}\\{1}'".Substitute(Environment.UserDomainName, Environment.UserName);
            return filter;
        }
        private string TeamProjectFilter() {
            var result = "\"TeamProject[{0}]\" <> null".Substitute(Translate("text()"));
            return result;
        }
        private string CheckinFilter() {
            var orderdCharsLower = SortChars(Project).ToLower();
            var orderdCharsUpper = SortChars(Project).ToUpper();

            var result = "\"Artifacts/Artifact[{0}]\" <> null"
                .Substitute(Translate("@TeamProject"));

            return result;
        }
        private string MyWorkItemChangedFilter() {
            var orderdCharsLower = SortChars(Project).ToLower();
            var orderdCharsUpper = SortChars(Project).ToUpper();

            var directoryEntry = new DirectoryEntry("WinNT://" + Environment.UserDomainName + "/" + Environment.UserName);
            var fullname = directoryEntry.Properties["fullName"].Value.ToString();

            var result = ("\"PortfolioProject[{0}]\" <> 'null' AND " +
                "(\"CoreFields/StringFields/Field[ReferenceName='System.AssignedTo']/OldValue\" = '{1}' OR " +
                "\"CoreFields/StringFields/Field[ReferenceName='System.AssignedTo']/NewValue\" = '{1}') AND " +
                "\"CoreFields/StringFields/Field[ReferenceName='System.AuthorizedAs']/NewValue\" <> '{1}'")
                .Substitute(Translate("text()"), fullname);

            return result;
        }

        /// <summary>
        /// The e-mail address to send the alert(s).
        /// </summary>
        [CommandSwitch(1, IsRequired = true)]
        public string Email { get; set; }

        /// <summary>
        /// The TFS collection.
        /// </summary>
        [CommandSwitch(2, IsRequired = true)]
        public string Collection { get; set; }

        /// <summary>
        /// The name of the TFS Team Project.
        /// </summary>
        [CommandSwitch(3)]
        public string Project { get; set; }

        /// <summary>
        /// The alerts to (un)subscribe. The list of options was generated using
        /// the same filters as the default set in VS 2010. 
        /// </summary>
        [CommandSwitch(4)]
        public AlertType[] Alerts { get; set; }

        /// <summary>
        /// Disable instead of enabling the alerts.
        /// </summary>
        [CommandSwitch(5)]
        public bool Disable { get; set; }
    }

    /// <summary>
    /// Dummy Command. Dummy Command. Dummy Command. Dummy Command. Dummy Command.
    /// Dummy Command. Dummy Command. Dummy Command. Dummy Command. Dummy Command.
    /// </summary>
    [Command]
    public class Dummy : Command {

        protected override IEnumerable<object> Execute(TextNodeContext context) {
            yield return ToString();
            throw new Exception("Hello World Exception!");
        }

        /// <summary>
        /// Optional Switch.
        /// </summary>
        [CommandSwitch(0)]
        public string OptionalSwitch { get; set; }

        /// <summary>
        /// Required Switch.
        /// </summary>
        [CommandSwitch(1, IsRequired = true)]
        public string RequiredSwitch { get; set; }

        /// <summary>
        /// Required Target.
        /// </summary>
        [CommandTarget(2)]
        public string RequiredTarget { get; set; }

        /// <summary>
        /// Optional Target.
        /// </summary>
        [CommandTarget(3, IsOptional = true)]
        public string OptionalTarget { get; set; }
    }
}
